🎖️GitЯра🎖️
Node / meshtastic / Meshtastic-Android / files / core / data / src / jvmTest / kotlin / org / meshtastic / core / data / repository / AirQualityChartReproTest.kt
Displaying Raw • Download
core/data/src/jvmTest/kotlin/org/meshtastic/core/data/repository/AirQualityChartReproTest.kt bfe3440a118861e800da83b57d7785b97642d597 (bfe3440a) Text, 10.81 KB
T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tff7b72package T7ee787org.meshtastic.core.data.repository
Tff7b72import T7ee787dev.mokkery.MockMode
Tff7b72import T7ee787dev.mokkery.answering.returns
Tff7b72import T7ee787dev.mokkery.every
Tff7b72import T7ee787dev.mokkery.mock
Tff7b72import T7ee787kotlinx.coroutines.flow.MutableStateFlow
Tff7b72import T7ee787kotlinx.coroutines.flow.first
Tff7b72import T7ee787kotlinx.coroutines.test.UnconfinedTestDispatcher
Tff7b72import T7ee787kotlinx.coroutines.test.advanceUntilIdle
Tff7b72import T7ee787kotlinx.coroutines.test.runTest
Tff7b72import T7ee787okio.ByteString.Companion.toByteString
Tff7b72import T7ee787org.meshtastic.core.data.datasource.NodeInfoReadDataSource
Tff7b72import T7ee787org.meshtastic.core.data.manager.MeshMessageProcessorImpl
Tff7b72import T7ee787org.meshtastic.core.database.entity.MyNodeEntity
Tff7b72import T7ee787org.meshtastic.core.di.CoroutineDispatchers
Tff7b72import T7ee787org.meshtastic.core.model.MeshLog
Tff7b72import T7ee787org.meshtastic.core.repository.FromRadioPacketHandler
Tff7b72import T7ee787org.meshtastic.core.repository.MeshDataHandler
Tff7b72import T7ee787org.meshtastic.core.repository.NodeManager
Tff7b72import T7ee787org.meshtastic.core.repository.ServiceStateWriter
Tff7b72import T7ee787org.meshtastic.core.testing.FakeDatabaseProvider
Tff7b72import T7ee787org.meshtastic.core.testing.FakeMeshLogPrefs
Tff7b72import T7ee787org.meshtastic.proto.AirQualityMetrics
Tff7b72import T7ee787org.meshtastic.proto.Data
Tff7b72import T7ee787org.meshtastic.proto.FromRadio
Tff7b72import T7ee787org.meshtastic.proto.MeshPacket
Tff7b72import T7ee787org.meshtastic.proto.PortNum
Tff7b72import T7ee787org.meshtastic.proto.Telemetry
Tff7b72import T7ee787kotlin.test.AfterTest
Tff7b72import T7ee787kotlin.test.Test
Tff7b72import T7ee787kotlin.test.assertEquals
Tff7b72import T7ee787kotlin.test.assertTrue
T8b949e/**
* Repro for the field report: a node's air-quality telemetry shows up in the in-app Debug log but never appears in the
* Air Quality chart (PR #5701).
*
* Uses Brian's real packet: a TRANSPORT_INTERNAL TELEMETRY_APP packet from the locally-connected node (num [localNum])
* carrying non-zero PM (pm10_standard=1, pm25_standard=2, pm100_standard=2).
*
* The chart reads [MeshLogRepositoryImpl.getTelemetryFrom], which resolves the viewed node through `effectiveLogId` to
* [MeshLog.NODE_NUM_LOCAL] and filters `WHERE from_num = :fromNum`. The Debug screen reads the unfiltered
* `getAllLogsUnbounded`. So the chart is sensitive to the stored `from_num` column; the Debug log is not.
*
* `MeshMessageProcessorImpl.processReceivedMeshPacket` stores `fromNum = if (packet.from == myNodeNum) NODE_NUM_LOCAL
* else packet.from`, where the insert-time `myNodeNum` is `nodeManager.myNodeNum.value` read at packet arrival
* (MeshServiceOrchestrator). That StateFlow starts null and is only set once MyNodeInfo is processed, so a local packet
* that arrives during the null window is stored under its raw `from_num` and orphaned from the chart while still
* visible in the Debug log.
*/
Tff7b72class T56d364AirQualityChartReproTest Tb4b4b4{
Tff7b72private Tff7b72lateinit Tff7b72var Te6edf3dbProviderTb4b4b4: Te6edf3FakeDatabaseProvider
Tff7b72private Tff7b72lateinit Tff7b72var Te6edf3meshLogPrefsTb4b4b4: Te6edf3FakeMeshLogPrefs
Tff7b72private Tff7b72lateinit Tff7b72var Te6edf3nodeInfoReadDataSourceTb4b4b4: Te6edf3NodeInfoReadDataSource
Tff7b72private Tff7b72val Te6edf3testDispatcher Tff7b72= Te6edf3UnconfinedTestDispatcherTb4b4b4(Tb4b4b4)
Tff7b72private Tff7b72val Te6edf3dispatchers Tff7b72= Te6edf3CoroutineDispatchersTb4b4b4(Te6edf3main Tff7b72= Te6edf3testDispatcherTb4b4b4, Te6edf3io Tff7b72= Te6edf3testDispatcherTb4b4b4, Te6edf3default Tff7b72= Te6edf3testDispatcherTb4b4b4)
Tff7b72private Tff7b72lateinit Tff7b72var Te6edf3repositoryTb4b4b4: Te6edf3MeshLogRepositoryImpl
Tff7b72private Tff7b72val Te6edf3nowMillis Tff7b72= T79c0ff1Te6edf3_000_000_000L
T8b949e/** Brian's connected node number, taken verbatim from his Debug log (`from=-93009324`). */
Tff7b72private Tff7b72val Te6edf3localNum Tff7b72= Tff7b72-T79c0ff9T79c0ff3T79c0ff0T79c0ff0T79c0ff9T79c0ff3T79c0ff2T79c0ff4
Tff7b72private Tff7b72fun Td2a8ffsetupTb4b4b4(Te6edf3myNodeNumTb4b4b4: Tffa657Int?Tb4b4b4) Tb4b4b4{
Te6edf3dbProvider Tff7b72= Te6edf3FakeDatabaseProviderTb4b4b4(Tb4b4b4)
Te6edf3meshLogPrefs Tff7b72= Te6edf3FakeMeshLogPrefsTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3apply Tb4b4b4{ Te6edf3setLoggingEnabledTb4b4b4(Tff7b72trueTb4b4b4) Tb4b4b4}
Te6edf3nodeInfoReadDataSource Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3nodeInfoReadDataSourceTb4b4b4.Te6edf3myNodeInfoFlowTb4b4b4(Tb4b4b4) Tb4b4b4} Te6edf3returns Te6edf3MutableStateFlowTb4b4b4(Te6edf3myNodeNumTff7b72?.Te6edf3letTb4b4b4(Tff7b72::Te6edf3myNodeEntityTb4b4b4)Tb4b4b4)
Te6edf3repository Tff7b72= Te6edf3MeshLogRepositoryImplTb4b4b4(Te6edf3dbProviderTb4b4b4, Te6edf3dispatchersTb4b4b4, Te6edf3meshLogPrefsTb4b4b4, Te6edf3nodeInfoReadDataSourceTb4b4b4)
Tb4b4b4}
Tf0883e@AfterTest
Tff7b72fun Td2a8fftearDownTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72if Tb4b4b4(Tff7b72::Te6edf3dbProviderTb4b4b4.Te6edf3isInitializedTb4b4b4) Te6edf3dbProviderTb4b4b4.Te6edf3closeTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tff7b72private Tff7b72fun Td2a8ffmyNodeEntityTb4b4b4(Te6edf3numTb4b4b4: Tffa657IntTb4b4b4) Tff7b72= Te6edf3MyNodeEntityTb4b4b4(
Te6edf3myNodeNum Tff7b72= Te6edf3numTb4b4b4,
Te6edf3model Tff7b72= Ta5d6ff"Ta5d6ffmodelTa5d6ff"Tb4b4b4,
Te6edf3firmwareVersion Tff7b72= Ta5d6ff"Ta5d6ff1.0Ta5d6ff"Tb4b4b4,
Te6edf3couldUpdate Tff7b72= Tff7b72falseTb4b4b4,
Te6edf3shouldUpdate Tff7b72= Tff7b72falseTb4b4b4,
Te6edf3currentPacketId Tff7b72= T79c0ff0LTb4b4b4,
Te6edf3messageTimeoutMsec Tff7b72= T79c0ff0Tb4b4b4,
Te6edf3minAppVersion Tff7b72= T79c0ff0Tb4b4b4,
Te6edf3maxChannels Tff7b72= T79c0ff0Tb4b4b4,
Te6edf3hasWifi Tff7b72= Tff7b72falseTb4b4b4,
Tb4b4b4)
T8b949e/** Brian's real reading: pm10_standard=1, pm25_standard=2, pm100_standard=2. */
Tff7b72private Tff7b72fun Td2a8ffbrianAirQualityTelemetryTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3TelemetryTb4b4b4(
Te6edf3air_quality_metrics Tff7b72=
Te6edf3AirQualityMetricsTb4b4b4(
Te6edf3pm10_standard Tff7b72= T79c0ff1Tb4b4b4,
Te6edf3pm25_standard Tff7b72= T79c0ff2Tb4b4b4,
Te6edf3pm100_standard Tff7b72= T79c0ff2Tb4b4b4,
Te6edf3pm10_environmental Tff7b72= T79c0ff1Tb4b4b4,
Te6edf3pm25_environmental Tff7b72= T79c0ff2Tb4b4b4,
Te6edf3pm100_environmental Tff7b72= T79c0ff2Tb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tff7b72private Tff7b72fun Td2a8ffairQualityPacketTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3MeshPacketTb4b4b4(
Te6edf3from Tff7b72= Te6edf3localNumTb4b4b4,
Te6edf3rx_time Tff7b72= T79c0ff1Te6edf3_700_000_000Tb4b4b4,
Te6edf3decoded Tff7b72=
Te6edf3DataTb4b4b4(Te6edf3payload Tff7b72= Te6edf3brianAirQualityTelemetryTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3encodeTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3toByteStringTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3portnum Tff7b72= Te6edf3PortNumTb4b4b4.Te6edf3TELEMETRY_APPTb4b4b4)Tb4b4b4,
Tb4b4b4)
Tff7b72private Tff7b72fun Td2a8ffairQualityLogTb4b4b4(Te6edf3fromNumTb4b4b4: Tffa657IntTb4b4b4) Tff7b72= Te6edf3MeshLogTb4b4b4(
Te6edf3uuid Tff7b72= Ta5d6ff"Ta5d6ffaq-Tffd700$Te6edf3fromNumTa5d6ff"Tb4b4b4,
Te6edf3message_type Tff7b72= Ta5d6ff"Ta5d6ffPacketTa5d6ff"Tb4b4b4,
Te6edf3received_date Tff7b72= Te6edf3nowMillisTb4b4b4,
Te6edf3raw_message Tff7b72= Ta5d6ff"Ta5d6ff"Tb4b4b4,
Te6edf3fromNum Tff7b72= Te6edf3fromNumTb4b4b4,
Te6edf3portNum Tff7b72= Te6edf3PortNumTb4b4b4.Te6edf3TELEMETRY_APPTb4b4b4.Te6edf3valueTb4b4b4,
Te6edf3fromRadio Tff7b72= Te6edf3FromRadioTb4b4b4(Te6edf3packet Tff7b72= Te6edf3airQualityPacketTb4b4b4(Tb4b4b4)Tb4b4b4)Tb4b4b4,
Tb4b4b4)
T8b949e/** Checkpoint 1: the parse + query round-trip preserves the air-quality payload (rules out content loss). */
Tf0883e@Test
Tff7b72fun Td2a8ff`checkpoint 1 - air quality payload survives the telemetry round-trip`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3setupTb4b4b4(Te6edf3myNodeNum Tff7b72= Tff7b72nullTb4b4b4) T8b949e// effectiveLogId(0) -> 0
Te6edf3repositoryTb4b4b4.Te6edf3insertTb4b4b4(Te6edf3airQualityLogTb4b4b4(Te6edf3fromNum Tff7b72= T79c0ff0Tb4b4b4)Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3repositoryTb4b4b4.Te6edf3getTelemetryFromTb4b4b4(T79c0ff0Tb4b4b4)Tb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3resultTb4b4b4.Te6edf3sizeTb4b4b4, Ta5d6ff"Ta5d6ffthe air-quality telemetry row should round-tripTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff2Tb4b4b4, Te6edf3resultTff7b72[T79c0ff0Tff7b72]Tb4b4b4.Te6edf3air_quality_metricsTff7b72?.Te6edf3pm25_standardTb4b4b4, Ta5d6ff"Ta5d6ffpm25_standard must survive decodeTa5d6ff"Tb4b4b4)
Tb4b4b4}
T8b949e/** Checkpoint 2: local-node air-quality stored under NODE_NUM_LOCAL is returned to the chart (the happy path). */
Tf0883e@Test
Tff7b72fun Td2a8ff`checkpoint 2 - local AQ stored under NODE_NUM_LOCAL is charted`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3setupTb4b4b4(Te6edf3myNodeNum Tff7b72= Te6edf3localNumTb4b4b4) T8b949e// viewing the local node -> effectiveLogId -> NODE_NUM_LOCAL
T8b949e// myNodeNum known at insert -> processReceivedMeshPacket would store NODE_NUM_LOCAL.
Te6edf3repositoryTb4b4b4.Te6edf3insertTb4b4b4(Te6edf3airQualityLogTb4b4b4(Te6edf3fromNum Tff7b72= Te6edf3MeshLogTb4b4b4.Te6edf3NODE_NUM_LOCALTb4b4b4)Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3repositoryTb4b4b4.Te6edf3getTelemetryFromTb4b4b4(Te6edf3localNumTb4b4b4)Tb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3resultTb4b4b4.Te6edf3sizeTb4b4b4, Ta5d6ff"Ta5d6ffAQ stored under NODE_NUM_LOCAL should be visible to the local node's chartTa5d6ff"Tb4b4b4)
Tb4b4b4}
T8b949e/**
* Checkpoint 3 (query invariant — the rationale for the insert-side fix): the per-node chart query keys the local
* node on NODE_NUM_LOCAL, so a row stored under the raw myNodeNum is not returned (though the unfiltered Debug log
* still shows it). This is *why* the insert path must key local packets under NODE_NUM_LOCAL — verified in
* checkpoint 4. This query behavior is intentional and unchanged by the fix.
*/
Tf0883e@Test
Tff7b72fun Td2a8ff`checkpoint 3 - query keys local node on NODE_NUM_LOCAL not the raw from_num`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3setupTb4b4b4(Te6edf3myNodeNum Tff7b72= Te6edf3localNumTb4b4b4) T8b949e// viewing the local node -> effectiveLogId -> NODE_NUM_LOCAL
Te6edf3repositoryTb4b4b4.Te6edf3insertTb4b4b4(Te6edf3airQualityLogTb4b4b4(Te6edf3fromNum Tff7b72= Te6edf3localNumTb4b4b4)Tb4b4b4) T8b949e// a hypothetical mis-keyed row
T8b949e// Debug screen (unfiltered) sees it:
Te6edf3assertEqualsTb4b4b4(
T79c0ff1Tb4b4b4,
Te6edf3repositoryTb4b4b4.Te6edf3getAllLogsUnboundedTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3sizeTb4b4b4,
Ta5d6ff"Ta5d6ffDebug log shows rows regardless of from_numTa5d6ff"Tb4b4b4,
Tb4b4b4)
T8b949e// Chart query (from_num = NODE_NUM_LOCAL) does not — hence the insert must never produce a raw-keyed local
T8b949e// row:
Tff7b72val Te6edf3charted Tff7b72= Te6edf3repositoryTb4b4b4.Te6edf3getTelemetryFromTb4b4b4(Te6edf3localNumTb4b4b4)Tb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3chartedTb4b4b4.Te6edf3isEmptyTb4b4b4(Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffthe local-node query only matches NODE_NUM_LOCALTa5d6ff"Tb4b4b4)
Tb4b4b4}
T8b949e/**
* Checkpoint 4 (regression test for the fix, end-to-end through the real insert path): a local air-quality packet
* received while myNodeNum is still null is BUFFERED (not stored under its raw from_num). Once myNodeNum resolves,
* the buffer flushes and the packet is stored under NODE_NUM_LOCAL, so the local node's Air Quality chart sees it.
*
* Before the fix this packet was stored immediately under its raw from_num and orphaned from the chart.
*/
Tf0883e@Test
Tff7b72fun Td2a8ff`checkpoint 4 - local AQ received before myNodeNum resolves is buffered then charted`Tb4b4b4(Tb4b4b4) Tff7b72=
Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3setupTb4b4b4(Te6edf3myNodeNum Tff7b72= Te6edf3localNumTb4b4b4) T8b949e// query side: phone is connected to localNum
Tff7b72val Te6edf3myNodeNumFlow Tff7b72= Te6edf3MutableStateFlowTff7b72<Tffa657Int?Tff7b72>Tb4b4b4(Tff7b72nullTb4b4b4) T8b949e// not yet resolved
Tff7b72val Te6edf3nodeManager Tff7b72= Te6edf3mockTff7b72<Te6edf3NodeManagerTff7b72>Tb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3nodeManagerTb4b4b4.Te6edf3isNodeDbReady Tb4b4b4} Te6edf3returns Te6edf3MutableStateFlowTb4b4b4(Tff7b72trueTb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3nodeManagerTb4b4b4.Te6edf3myNodeNum Tb4b4b4} Te6edf3returns Te6edf3myNodeNumFlow
Tff7b72val Te6edf3processor Tff7b72=
Te6edf3MeshMessageProcessorImplTb4b4b4(
Te6edf3nodeManager Tff7b72= Te6edf3nodeManagerTb4b4b4,
Te6edf3serviceStateWriter Tff7b72= Te6edf3mockTff7b72<Te6edf3ServiceStateWriterTff7b72>Tb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)Tb4b4b4,
Te6edf3meshLogRepository Tff7b72= Te6edf3lazy Tb4b4b4{ Te6edf3repository Tb4b4b4}Tb4b4b4,
Te6edf3dataHandler Tff7b72= Te6edf3lazy Tb4b4b4{ Te6edf3mockTff7b72<Te6edf3MeshDataHandlerTff7b72>Tb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4) Tb4b4b4}Tb4b4b4,
Te6edf3fromRadioDispatcher Tff7b72= Te6edf3mockTff7b72<Te6edf3FromRadioPacketHandlerTff7b72>Tb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)Tb4b4b4,
Te6edf3scope Tff7b72= Te6edf3backgroundScopeTb4b4b4,
Tb4b4b4)
T8b949e// Arrives before MyNodeInfo resolves -> buffered, NOT written to the log table (so not orphaned in the DB).
Te6edf3processorTb4b4b4.Te6edf3handleReceivedMeshPacketTb4b4b4(Te6edf3airQualityPacketTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3myNodeNum Tff7b72= Tff7b72nullTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff0Tb4b4b4, Te6edf3repositoryTb4b4b4.Te6edf3getAllLogsUnboundedTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3sizeTb4b4b4, Ta5d6ff"Ta5d6ffpacket should be buffered, not yet storedTa5d6ff"Tb4b4b4)
T8b949e// MyNodeInfo resolves -> the buffer flushes and the packet is stored under NODE_NUM_LOCAL.
Te6edf3myNodeNumFlowTb4b4b4.Te6edf3value Tff7b72= Te6edf3localNum
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3charted Tff7b72= Te6edf3repositoryTb4b4b4.Te6edf3getTelemetryFromTb4b4b4(Te6edf3localNumTb4b4b4)Tb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3chartedTb4b4b4.Te6edf3sizeTb4b4b4, Ta5d6ff"Ta5d6ffafter myNodeNum resolves, the local AQ packet must reach the chartTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(
T79c0ff2Tb4b4b4,
Te6edf3chartedTff7b72[T79c0ff0Tff7b72]Tb4b4b4.Te6edf3air_quality_metricsTff7b72?.Te6edf3pm25_standardTb4b4b4,
Ta5d6ff"Ta5d6ffthe real reading (pm25_standard=2) survivesTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
Served by rngit 1.5.0 - Generated in 0.14s